﻿using System;
using UnityEngine;
using UnityEngine.AI;

namespace AI.Graph {


    [RequireComponent (typeof (NavMeshAgent))]
    public class NavMeshMovement : MonoBehaviour {

#region Fields

        private const float stopDistanceProportion = 0.1F;

        [SerializeField]
        private Animator animator = null;

        [SerializeField]
        private string animatorSpeedParameter = "Speed";

        private NavMeshAgent m_agent;
        private Vector3 m_destination;
        private Action m_onDestinationReached;
        private int m_hashSpeedParameter;

        public float turnSmoothing = 15f;
        public float speedDampTime = 0.1f;
        public float turnSpeedThreshold = 0.5f;

#endregion


#region Properties

        public Vector3 destination {
            get { return m_destination; }
        }


        public float speed {
            get { return m_agent.speed; }
            set { m_agent.speed = value; }
        }


        public float stoppingDistance {
            get { return m_agent.stoppingDistance; }
        }

        public float remainingDistance {
            get { return m_agent.remainingDistance; }
        }

        public bool isMoving {
            get { return m_agent.remainingDistance > 0.01F; }
        }

#endregion


#region Unity

        void Awake () {
            m_agent = GetComponent<NavMeshAgent> ();
            m_agent.updateRotation = false;

            m_hashSpeedParameter = Animator.StringToHash (animatorSpeedParameter);

            m_destination = transform.position;
        }


        void Update () {
            if (m_agent.pathPending) return;

            if (m_agent.remainingDistance <= m_agent.stoppingDistance) {
                m_agent.isStopped = true;

                if (m_onDestinationReached != null) {
                    m_onDestinationReached ();
                    m_onDestinationReached = null;
                }

                Stop ();
            }

            float speed = m_agent.desiredVelocity.magnitude;


            if (m_agent.remainingDistance <= m_agent.stoppingDistance * stopDistanceProportion) {
                Stopping (out speed);
            }
            else if (speed > turnSpeedThreshold) {
                Moving ();
            }

            if (animator) {
                animator.SetFloat (m_hashSpeedParameter, speed, speedDampTime, Time.deltaTime);
            }
        }


        void OnAnimatorMove () {
            if (animator) {
                m_agent.velocity = animator.deltaPosition / Time.deltaTime;
            }
        }

#endregion


        void Stopping (out float speed) {
            m_agent.isStopped = true;

            speed = 0;
        }


        void Moving () {
            Quaternion targetRotation = Quaternion.LookRotation (m_agent.desiredVelocity);
            transform.rotation = Quaternion.Lerp (transform.rotation, targetRotation, turnSmoothing * Time.deltaTime);
        }


        public void RotateToMouse (bool immediate = false) {
            Plane playerPlane = new Plane (Vector3.up, transform.position);
            Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);

            float hitdist;
            if (playerPlane.Raycast (ray, out hitdist)) {
                Vector3 targetPoint = ray.GetPoint (hitdist);
                RotateToDestination (targetPoint, immediate);
            }
        }


        public void RotateToDestination (Vector3 destination, bool immediate = false) {
            Quaternion targetRotation = Quaternion.LookRotation (destination - transform.position);
            if (immediate) {
                transform.rotation = targetRotation;
            }
            else {
                transform.rotation = Quaternion.Lerp (transform.rotation, targetRotation, turnSmoothing * Time.deltaTime);
            }
        }


        public void MoveToDestination (Vector3 point, float stoppingDistance = 0) {
            MoveToDestination (point, null, stoppingDistance);
        }


        public void MoveToDestination (Vector3 point, Action onDestinationReached, float stoppingDistance = 0) {
            Stop ();

            m_destination = point;
            m_agent.stoppingDistance = stoppingDistance;
            m_agent.SetDestination (point);
            m_agent.isStopped = false;
            m_onDestinationReached = onDestinationReached;
        }


        public void Stop () {
            m_agent.isStopped = true;
            m_agent.stoppingDistance = 0;
        }


        public bool IsValidDestination (Vector3 destination) {
            if (!m_agent.enabled) return false;

            return m_agent.IsValidDestination (destination);
        }


        public bool SamplePosition (Vector3 destination, out Vector3 sampledPosition, float distance = 100) {
            NavMeshHit hit;
            if (NavMesh.SamplePosition (destination, out hit, distance, m_agent.areaMask)) {
                sampledPosition = hit.position;
                return true;
            }

            sampledPosition = Vector3.zero;
            return false;
        }


        public void WarpToDestination (Vector3 destination) {
            NavMeshHit hit;
            NavMesh.SamplePosition (destination, out hit, 100, m_agent.areaMask);
            m_agent.Warp (hit.position);
        }


        public void RotateToMouseImmediate () {
            RotateToMouse (true);
        }


        public bool NearestDestination (Vector3[] destinations, out int destinationIndex) {
            return m_agent.NearestDestination (destinations, out destinationIndex);
        }


        public bool NearestDestination (Vector3[] destinations, out Vector3 destination) {
            return m_agent.NearestDestination (destinations, out destination);
        }


        public bool NearestDestination (Vector3[] destinations, out Vector3 destination, out int destinationIndex) {
            return m_agent.NearestDestination (destinations, out destination, out destinationIndex);
        }

    }


}